From: Matteo Collina Date: Tue, 23 Jun 2026 10:47:47 +0000 (+0200) Subject: https: bind identity checks to session reuse X-Git-Tag: archive/raspbian/18.20.4+dfsg-1_deb12u3+rpi1^2~2 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=62590443d6a45d15b6a14870c5f92e9b492e34fb;p=nodejs.git https: bind identity checks to session reuse PR-URL: https://github.com/nodejs-private/node-private/pull/934 Reviewed-By: Rafael Gonzaga CVE-ID: CVE-2026-58040 origin: backport, https://github.com/nodejs/node/commit/51123159fe863073d7dcaf5c88b23bad8aae1a62 Gbp-Pq: Topic sec Gbp-Pq: Name CVE-2026-58040.patch --- diff --git a/lib/https.js b/lib/https.js index 818f95d5a..641981e98 100644 --- a/lib/https.js +++ b/lib/https.js @@ -34,6 +34,7 @@ const { ObjectSetPrototypeOf, ReflectApply, ReflectConstruct, + Symbol, } = primordials; const { @@ -44,6 +45,8 @@ assertCrypto(); const tls = require('tls'); const { Agent: HttpAgent } = require('_http_agent'); +const kPerRequestCheckServerIdentity = Symbol('per-request checkServerIdentity'); +let perRequestCheckServerIdentityIndex = 0; const { httpServerPreClose, Server: HttpServer, @@ -145,7 +148,9 @@ function createConnection(port, host, options) { debug('createConnection', options); - if (options._agentKey) { + const reuseSession = options._agentKey && + !options[kPerRequestCheckServerIdentity]; + if (reuseSession) { const session = this._getSession(options._agentKey); if (session) { debug('reuse session for %j', options._agentKey); @@ -158,7 +163,10 @@ function createConnection(port, host, options) { const socket = tls.connect(options); - if (options._agentKey) { + if (options[kPerRequestCheckServerIdentity]) + socket[kPerRequestCheckServerIdentity] = true; + + if (reuseSession) { // Cache new session for reuse socket.on('session', (session) => { this._cacheSession(options._agentKey, session); @@ -208,6 +216,12 @@ function Agent(options) { ObjectSetPrototypeOf(Agent.prototype, HttpAgent.prototype); ObjectSetPrototypeOf(Agent, HttpAgent); Agent.prototype.createConnection = createConnection; +Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { + if (socket[kPerRequestCheckServerIdentity]) + return false; + + return FunctionPrototypeCall(HttpAgent.prototype.keepSocketAlive, this, socket); +}; function getPfxAgentKey(pfx, passphrase) { if (!ArrayIsArray(pfx)) @@ -316,6 +330,9 @@ Agent.prototype.getName = function getName(options = kEmptyObject) { if (options.privateKeyEngine) name += options.privateKeyEngine; + if (options[kPerRequestCheckServerIdentity]) + name += `:${options[kPerRequestCheckServerIdentity]}`; + return name; }; @@ -356,6 +373,22 @@ Agent.prototype._evictSession = function _evictSession(key) { const globalAgent = new Agent(); +function hasAgentCheckServerIdentity(options) { + let { agent } = options; + if (agent === false) + return false; + + if (agent === null || agent === undefined) { + if (typeof options.createConnection === 'function') + return false; + agent = module.exports.globalAgent; + } + + return agent?.options?.checkServerIdentity !== undefined; +} + + + /** * Makes a request to a secure web server. * @param {...any} args @@ -375,6 +408,13 @@ function request(...args) { ObjectAssign(options, ArrayPrototypeShift(args)); } + if (options.checkServerIdentity !== undefined && + options.checkServerIdentity !== tls.checkServerIdentity && + !hasAgentCheckServerIdentity(options)) { + options[kPerRequestCheckServerIdentity] = + ++perRequestCheckServerIdentityIndex; + } + options._defaultAgent = module.exports.globalAgent; ArrayPrototypeUnshift(args, options); diff --git a/test/parallel/test-https-agent-checkserveridentity-reuse.js b/test/parallel/test-https-agent-checkserveridentity-reuse.js new file mode 100644 index 000000000..2c339c33f --- /dev/null +++ b/test/parallel/test-https-agent-checkserveridentity-reuse.js @@ -0,0 +1,113 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const https = require('https'); +const { once } = require('events'); + +const key = fixtures.readKey('agent1-key.pem'); +const cert = fixtures.readKey('agent1-cert.pem'); +const ca = fixtures.readKey('ca1-cert.pem'); +const expectedError = /rejected by callback/; + +function request(options) { + return new Promise((resolve, reject) => { + const req = https.get({ + host: '127.0.0.1', + servername: 'agent1', + ca: [ca], + ...options, + }, (res) => { + const socket = res.socket; + res.resume(); + res.on('end', () => resolve({ + socket, + reusedSocket: req.reusedSocket, + })); + }); + + req.on('error', reject); + }); +} + +const server = https.createServer({ + key, + cert, + minVersion: 'TLSv1.2', + maxVersion: 'TLSv1.2', +}, (req, res) => { + res.end('ok'); +}); + +(async function() { + server.listen(0); + await once(server, 'listening'); + + const port = server.address().port; + let acceptCalls = 0; + let rejectCalls = 0; + const acceptingCheck = () => { + acceptCalls++; + }; + const rejectingCheck = () => { + rejectCalls++; + return new Error('rejected by callback'); + }; + + const sessionAgent = new https.Agent(); + const keepAliveAgent = new https.Agent({ + keepAlive: true, + maxCachedSessions: 0, + }); + const agentLevelAgent = new https.Agent({ + checkServerIdentity: acceptingCheck, + }); + + try { + await request({ + port, + agent: sessionAgent, + checkServerIdentity: acceptingCheck, + }); + assert.deepStrictEqual(sessionAgent._sessionCache.map, {}); + await assert.rejects(request({ + port, + agent: sessionAgent, + checkServerIdentity: rejectingCheck, + }), expectedError); + + await request({ + port, + agent: keepAliveAgent, + checkServerIdentity: acceptingCheck, + }); + await assert.rejects(request({ + port, + agent: keepAliveAgent, + checkServerIdentity: rejectingCheck, + }), expectedError); + + const first = await request({ + port, + agent: agentLevelAgent, + }); + assert.strictEqual(first.socket.isSessionReused(), false); + const second = await request({ + port, + agent: agentLevelAgent, + }); + assert.strictEqual(second.socket.isSessionReused(), true); + + assert.strictEqual(acceptCalls, 3); + assert.strictEqual(rejectCalls, 2); + } finally { + sessionAgent.destroy(); + keepAliveAgent.destroy(); + agentLevelAgent.destroy(); + server.close(); + await once(server, 'close'); + } +})().then(common.mustCall());